/*
	This file is part of go-palletone.
	go-palletone is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	go-palletone is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	You should have received a copy of the GNU General Public License
	along with go-palletone.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
* Copyright IBM Corp. All Rights Reserved.
* @author PalletOne core developers <dev@pallet.one>
* @date 2018
 */

package ccprovider

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/golang/protobuf/proto"
	"github.com/palletone/go-palletone/common/log"
	pb "github.com/palletone/go-palletone/core/vmContractPub/protos/peer"
	"time"
)

//var log = flogging.MustGetLogger("ccprovider")

var chaincodeInstallPath string

//CCPackage encapsulates a chaincode package which can be
//    raw ChaincodeDeploymentSpec
//    SignedChaincodeDeploymentSpec
// Attempt to keep the interface at a level with minimal
// interface for possible generalization.
type CCPackage interface {
	//InitFromBuffer initialize the package from bytes
	InitFromBuffer(buf []byte) (*ChaincodeData, error)

	// InitFromFS gets the chaincode from the filesystem (includes the raw bytes too)
	InitFromFS(ccname string, ccversion string) ([]byte, *pb.PtnChaincodeDeploymentSpec, error)

	// PutChaincodeToFS writes the chaincode to the filesystem
	PutChaincodeToFS() error

	// GetDepSpec gets the ChaincodeDeploymentSpec from the package
	GetDepSpec() *pb.PtnChaincodeDeploymentSpec

	// GetDepSpecBytes gets the serialized ChaincodeDeploymentSpec from the package
	GetDepSpecBytes() []byte

	// ValidateCC validates and returns the chaincode deployment spec corresponding to
	// ChaincodeData. The validation is based on the metadata from ChaincodeData
	// One use of this method is to validate the chaincode before launching
	ValidateCC(ccdata *ChaincodeData) error

	// GetPackageObject gets the object as a proto.Message
	GetPackageObject() proto.Message

	// GetChaincodeData gets the ChaincodeData
	GetChaincodeData() *ChaincodeData

	// GetId gets the fingerprint of the chaincode based on package computation
	GetId() []byte
}

//SetChaincodesPath sets the chaincode path for this peer
//func SetChaincodesPath(path string) error {
//	if s, err := os.Stat(path); err != nil {
//		if os.IsNotExist(err) {
//			if err := os.MkdirAll(path, 0750); err != nil {
//				log.Errorf("Could not create chaincodes install path: %s", err)
//				return err
//			}
//		} else {
//			log.Errorf("Could not stat chaincodes install path: %s", err)
//		}
//	} else if !s.IsDir() {
//		log.Errorf("chaincode path exists but not a dir: %s", path)
//		return errors.New("chaincodes path is not dir")
//	}
//
//	chaincodeInstallPath = path
//	return nil
//}

//GetChaincodePackage returns the chaincode package from the file system
//func GetChaincodePackage(ccname string, ccversion string) ([]byte, error) {
//	path := fmt.Sprintf("%s/%s.%s", chaincodeInstallPath, ccname, ccversion)
//	log.Infof("path:%s", path)
//	var ccbytes []byte
//	var err error
//	if ccbytes, err = ioutil.ReadFile(path); err != nil {
//		return nil, err
//	}
//	return ccbytes, nil
//}

//ChaincodePackageExists returns whether the chaincode package exists in the file system
func ChaincodePackageExists(ccname string, ccversion string) (bool, error) {
	path := filepath.Join(chaincodeInstallPath, ccname+"."+ccversion)
	_, err := os.Stat(path)
	if err == nil {
		// chaincodepackage already exists
		return true, nil
	}
	return false, err
}

type CCCacheSupport interface {
	//GetChaincode is needed by the cache to get chaincode data
	GetChaincode(ccname string, ccversion string) (CCPackage, error)
}

// CCInfoFSImpl provides the implementation for CC on the FS and the access to it
// It implements CCCacheSupport
type CCInfoFSImpl struct{}

// GetChaincodeFromFS this is a wrapper for hiding package implementation.
func (*CCInfoFSImpl) GetChaincode(ccname string, ccversion string) (CCPackage, error) {
	//try raw CDS
	//cccdspack := &CDSPackage{}
	//_, _, err := cccdspack.InitFromFS(ccname, ccversion)
	//
	////glh
	///*
	//	if err != nil {
	//		//try signed CDS
	//		ccscdspack := &SignedCDSPackage{}
	//		_, _, err = ccscdspack.InitFromFS(ccname, ccversion)
	//		if err != nil {
	//			return nil, err
	//		}
	//		return ccscdspack, nil
	//	}
	//*/
	//return cccdspack, err
	return nil, nil
}

// PutChaincodeIntoFS is a wrapper for putting raw ChaincodeDeploymentSpec
//using CDSPackage. This is only used in UTs
//func (*CCInfoFSImpl) PutChaincode(depSpec *pb.PtnChaincodeDeploymentSpec) (CCPackage, error) {
//	buf, err := proto.Marshal(depSpec)
//	if err != nil {
//		return nil, err
//	}
//	cccdspack := &CDSPackage{}
//	if _, err := cccdspack.InitFromBuffer(buf); err != nil {
//		return nil, err
//	}
//	err = cccdspack.PutChaincodeToFS()
//	if err != nil {
//		return nil, err
//	}
//
//	return cccdspack, nil
//}

// The following lines create the cache of CCPackage data that sits
// on top of the file system and avoids a trip to the file system
// every time. The cache is disabled by default and only enabled
// if EnableCCInfoCache is called. This is an unfortunate hack
// required by some legacy tests that remove chaincode packages
// from the file system as a means of simulating particular test
// conditions. This way of testing is incompatible with the
// immutable nature of chaincode packages that is assumed by hlf v1
// and implemented by this cache. For this reason, tests are for now
// allowed to run with the cache disabled (unless they enable it)
// until a later time in which they are fixed. The peer process on
// the other hand requires the benefits of this cache and therefore
// enables it.

// ccInfoFSStorageMgr is the storage manager used either by the cache or if the
// cache is bypassed
var ccInfoFSProvider = &CCInfoFSImpl{}

// ccInfoCache is the cache instance itself
var ccInfoCache = NewCCInfoCache(ccInfoFSProvider)

// ccInfoCacheEnabled keeps track of whether the cache is enable
// (it is disabled by default)
var ccInfoCacheEnabled bool

// EnableCCInfoCache can be called to enable the cache
func EnableCCInfoCache() {
	ccInfoCacheEnabled = true
}

// GetChaincodeFromFS retrieves chaincode information from the file system
func GetChaincodeFromFS(ccname string, ccversion string) (CCPackage, error) {
	return ccInfoFSProvider.GetChaincode(ccname, ccversion)
}

// PutChaincodeIntoFS puts chaincode information in the file system (and
// also in the cache to prime it) if the cache is enabled, or directly
// from the file system otherwise
//func PutChaincodeIntoFS(depSpec *pb.PtnChaincodeDeploymentSpec) error {
//	_, err := ccInfoFSProvider.PutChaincode(depSpec)
//	return err
//}

// GetChaincodeData gets chaincode data from cache if there's one
func GetChaincodeData(ccname string, ccversion string) (*ChaincodeData, error) {
	if ccInfoCacheEnabled {
		log.Debugf("Getting chaincode data for <%s, %s> from cache", ccname, ccversion)
		return ccInfoCache.GetChaincodeData(ccname, ccversion)
	}
	if ccpack, err := ccInfoFSProvider.GetChaincode(ccname, ccversion); err != nil {
		return nil, err
	} else {
		log.Infof("Putting chaincode data for <%s, %s> into cache", ccname, ccversion)
		return ccpack.GetChaincodeData(), nil
	}
}

func CheckInstantiationPolicy(name, version string, cdLedger *ChaincodeData) error {
	ccdata, err := GetChaincodeData(name, version)
	if err != nil {
		return err
	}

	// we have the info from the fs, check that the policy
	// matches the one on the file system if one was specified;
	// this check is required because the admin of this peer
	// might have specified instantiation policies for their
	// chaincode, for example to make sure that the chaincode
	// is only instantiated on certain channels; a malicious
	// peer on the other hand might have created a deploy
	// transaction that attempts to bypass the instantiation
	// policy. This check is there to ensure that this will not
	// happen, i.e. that the peer will refuse to invoke the
	// chaincode under these conditions. More info on
	if ccdata.InstantiationPolicy != nil {
		if !bytes.Equal(ccdata.InstantiationPolicy, cdLedger.InstantiationPolicy) {
			return fmt.Errorf("Instantiation policy mismatch for cc %s/%s", name, version)
		}
	}

	return nil
}

// GetCCPackage tries each known package implementation one by one
// till the right package is found
func GetCCPackage(buf []byte) (CCPackage, error) {
	//try raw CDS
	//cccdspack := &CDSPackage{}
	//_, err := cccdspack.InitFromBuffer(buf)
	//return cccdspack, err
	return nil, nil
	//glh
	/*
		if _, err := cccdspack.InitFromBuffer(buf); err != nil {
			//try signed CDS
			ccscdspack := &SignedCDSPackage{}
			if _, err := ccscdspack.InitFromBuffer(buf); err != nil {
				return nil, err
			}
			return ccscdspack, nil
		}
		return cccdspack, nil
	*/
}

// GetInstalledChaincodes returns a map whose key is the chaincode id and
// value is the ChaincodeDeploymentSpec struct for that chaincodes that have
// been installed (but not necessarily instantiated) on the peer by searching
// the chaincode install path
func GetInstalledChaincodes() (*pb.PtnChaincodeQueryResponse, error) {
	files, err := ioutil.ReadDir(chaincodeInstallPath)
	if err != nil {
		return nil, err
	}

	// array to store info for all chaincode entries from LSCC
	var ccInfoArray []*pb.PtnChaincodeInfo

	for _, file := range files {
		// split at first period as chaincode versions can contain periods while
		// chaincode names cannot
		fileNameArray := strings.SplitN(file.Name(), ".", 2)

		// check that length is 2 as expected, otherwise skip to next cc file
		if len(fileNameArray) == 2 {
			ccname := fileNameArray[0]
			ccversion := fileNameArray[1]
			ccpack, err := GetChaincodeFromFS(ccname, ccversion)
			if err != nil {
				// either chaincode on filesystem has been tampered with or
				// a non-chaincode file has been found in the chaincodes directory
				log.Errorf("Unreadable chaincode file found on filesystem: %s", file.Name())
				continue
			}

			cdsfs := ccpack.GetDepSpec()

			name := cdsfs.GetChaincodeSpec().GetChaincodeId().Name
			version := cdsfs.GetChaincodeSpec().GetChaincodeId().Version
			if name != ccname || version != ccversion {
				// chaincode name/version in the chaincode file name has been modified
				// by an external entity
				log.Errorf("Chaincode file's name/version has been modified on the filesystem: %s", file.Name())
				continue
			}

			path := cdsfs.GetChaincodeSpec().ChaincodeId.Path
			// since this is just an installed chaincode these should be blank
			input, escc, vscc := "", "", ""

			ccInfo := &pb.PtnChaincodeInfo{Name: name, Version: version, Path: path, Input: input, Escc: escc, Vscc: vscc, Id: ccpack.GetId()}

			// add this specific chaincode's metadata to the array of all chaincodes
			ccInfoArray = append(ccInfoArray, ccInfo)
		}
	}
	// add array with info about all instantiated chaincodes to the query
	// response proto
	cqr := &pb.PtnChaincodeQueryResponse{Chaincodes: ccInfoArray}

	return cqr, nil
}

//CCContext pass this around instead of string of args
type CCContext struct {
	//contract id
	ContractId []byte
	//ChainID chain id
	ChainID string

	//Name chaincode name
	Name string

	//Version used to construct the chaincode image and register
	Version string

	//TxID is the transaction id for the proposal (if any)
	TxID string

	//Syscc is this a system chaincode
	Syscc bool

	//SignedProposal for this invoke (if any)
	//this is kept here for access control and in case we need to pass something
	//from this to the chaincode
	SignedProposal *pb.PtnSignedProposal

	//Proposal for this invoke (if any)
	//this is kept here just in case we need to pass something
	//from this to the chaincode
	Proposal *pb.PtnProposal

	//this is not set but computed (note that this is not exported. use GetCanonicalName)
	canonicalName string

	// this is additional data passed to the chaincode
	ProposalDecorations map[string][]byte
}

//NewCCContext just construct a new struct with whatever args
func NewCCContext(contractid []byte, cid, name, version, txid string, syscc bool, signedProp *pb.PtnSignedProposal, prop *pb.PtnProposal) *CCContext {
	//version CANNOT be empty. The chaincode namespace has to use version and chain name.
	//All system chaincodes share the same version given by utils.GetSysCCVersion. Note
	//that neither Chain Name or Version are stored in a chaincodes state on the ledger
	if version == "" {
		log.Errorf("empty version(chain=%s,chaincode=%s,version=%s,txid=%s,syscc=%t,proposal=%p", cid, name, version, txid, syscc, prop)
		return nil
	}

	canName := name + ":" + version
	cccid := &CCContext{contractid, cid, name, version, txid, syscc, signedProp, prop, canName, nil}

	log.Debugf("NewCCCC (chain[%s],chaincode[%s],version[%s],txid[%s],syscc[%t],proposal[%p],canname[%s])", cid, name, version, txid, syscc, prop, cccid.canonicalName)

	return cccid
}

//GetCanonicalName returns the canonical name associated with the proposal context
func (cccid *CCContext) GetCanonicalName() string {
	if cccid.canonicalName == "" {
		panic(fmt.Sprintf("cccid not constructed using NewCCContext(chain=%s,chaincode=%s,version=%s,txid=%s,syscc=%t)", cccid.ChainID, cccid.Name, cccid.Version, cccid.TxID, cccid.Syscc))
	}

	return cccid.canonicalName
}

func (cccid *CCContext) GetContainerName() string {
	name := cccid.Name + ":" + cccid.Version
	return strings.Replace(name, ":", "-", -1)
}

//-------- ChaincodeData is stored on the LSCC -------

//ChaincodeData defines the datastructure for chaincodes to be serialized by proto
//Type provides an additional check by directing to use a specific package after instantiation
//Data is Type specifc (see CDSPackage and SignedCDSPackage)
type ChaincodeData struct {
	//Name of the chaincode
	Name string `protobuf:"bytes,1,opt,name=name"`

	//Version of the chaincode
	Version string `protobuf:"bytes,2,opt,name=version"`

	//Escc for the chaincode instance
	Escc string `protobuf:"bytes,3,opt,name=escc"`

	//Vscc for the chaincode instance
	Vscc string `protobuf:"bytes,4,opt,name=vscc"`

	//Policy endorsement policy for the chaincode instance
	Policy []byte `protobuf:"bytes,5,opt,name=policy,proto3"`

	//Data data specific to the package
	Data []byte `protobuf:"bytes,6,opt,name=data,proto3"`

	//Id of the chaincode that's the unique fingerprint for the CC
	//This is not currently used anywhere but serves as a good
	//eyecatcher
	Id []byte `protobuf:"bytes,7,opt,name=id,proto3"`

	//InstantiationPolicy for the chaincode
	InstantiationPolicy []byte `protobuf:"bytes,8,opt,name=instantiation_policy,proto3"`
}

// implement functions needed by resourcesconfig.ChaincodeDefinition

// CCName returns the name of this chaincode (the name it was put in the ChaincodeRegistry with).
func (cd *ChaincodeData) CCName() string {
	return cd.Name
}

// Hash returns the hash of the chaincode.
func (cd *ChaincodeData) Hash() []byte {
	return cd.Id
}

// CCVersion returns the version of the chaincode.
func (cd *ChaincodeData) CCVersion() string {
	return cd.Version
}

// Validation returns how to validate transactions for this chaincode.
// The string returned is the name of the validation method (usually 'vscc')
// and the bytes returned are the argument to the validation (in the case of
// 'vscc', this is a marshaled pb.VSCCArgs message).
func (cd *ChaincodeData) Validation() (string, []byte) {
	return cd.Vscc, cd.Policy
}

// Endorsement returns how to endorse proposals for this chaincode.
// The string returns is the name of the endorsement method (usually 'escc').
func (cd *ChaincodeData) Endorsement() string {
	return cd.Escc
}

//implement functions needed from proto.Message for proto's mar/unmarshal functions

//Reset resets
func (cd *ChaincodeData) Reset() { *cd = ChaincodeData{} }

//String converts to string
func (cd *ChaincodeData) String() string { return proto.CompactTextString(cd) }

//ProtoMessage just exists to make proto happy
func (*ChaincodeData) ProtoMessage() {}

// ChaincodeProvider provides an abstraction layer that is
// used for different packages to interact with code in the
// chaincode package without importing it; more methods
// should be added below if necessary
type ChaincodeProvider interface {
	// GetContext returns a ledger context and a tx simulator; it's the
	// caller's responsibility to release the simulator by calling its
	// done method once it is no longer useful
	//glh
	//GetContext(ledger ledger.PeerLedger, txid string) (context.Context, ledger.TxSimulator, error)
	GetContext() (context.Context, error)
	// GetCCContext returns an opaque chaincode context
	GetCCContext(contractid []byte, cid, name, version, txid string, syscc bool, signedProp *pb.PtnSignedProposal, prop *pb.PtnProposal) interface{}
	// ExecuteChaincode executes the chaincode given context and args
	ExecuteChaincode(ctxt context.Context, cccid interface{}, args [][]byte, timeout time.Duration) (*pb.PtnResponse, *pb.PtnChaincodeEvent, error)
	// Execute executes the chaincode given context and spec (invocation or deploy)
	Execute(ctxt context.Context, cccid interface{}, spec interface{}, timeout time.Duration) (*pb.PtnResponse, *pb.PtnChaincodeEvent, error)
	// ExecuteWithErrorFilter executes the chaincode given context and spec and returns payload
	ExecuteWithErrorFilter(ctxt context.Context, cccid interface{}, spec interface{}, timeout time.Duration) ([]byte, *pb.PtnChaincodeEvent, error)
	// Stop stops the chaincode given context and deployment spec
	Stop(ctxt context.Context, cccid interface{}, spec *pb.PtnChaincodeDeploymentSpec, dontRmCon bool) error

	Destroy(ctxt context.Context, cccid interface{}, spec *pb.PtnChaincodeDeploymentSpec) error
}

var ccFactory ChaincodeProviderFactory

// ChaincodeProviderFactory defines a factory interface so
// that the actual implementation can be injected
type ChaincodeProviderFactory interface {
	NewChaincodeProvider() ChaincodeProvider
}

// RegisterChaincodeProviderFactory is to be called once to set
// the factory that will be used to obtain instances of ChaincodeProvider
func RegisterChaincodeProviderFactory(ccfact ChaincodeProviderFactory) {
	ccFactory = ccfact
}

// GetChaincodeProvider returns instances of ChaincodeProvider;
// the actual implementation is controlled by the factory that
// is registered via RegisterChaincodeProviderFactory
func GetChaincodeProvider() ChaincodeProvider {
	if ccFactory == nil {
		panic("The factory must be set first via RegisterChaincodeProviderFactory")
	}
	return ccFactory.NewChaincodeProvider()
}
